/*
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU General Public License
 *  as published by the Free Software Foundation; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Description:
 *   The ncurses interface.
 */

#include <errno.h>
#define NCURSES_ENABLE_STDBOOL_H 0 // conflicts with doomtype.h dboolean
#include <form.h>
#include <math.h> // atan and sqrt
#include <stddef.h>
#include "SDL.h"
#include "SDL_thread.h"
#include "x_gui.h"
#include "x_brute.h"
#include "x_keys.h"
#include "x_main.h"
#include "x_saves.h"
#include "doomstat.h"
#include "d_event.h"
#include "d_ticcmd.h"
#include "e6y.h"
#include "m_random.h"

#define STR(x) x_removeSpaces(field_buffer(x, 0))
#define CALCYX(y, x) x_calcLines(y), x_calcCols(x)
#define F_ARGLEN_IS_ARG 1
#define F_NEG_ARG       2
#define TIC_LEN         30
#define DEFLINES        24
#define DEFCOLS         80

#define TICLIST_COLS 19
// 5 fields for tics, 12 for checks, 1 is options
#define BRUTE_FIELDS (6 + MAXVALUES + 2 + 1)

extern int xDoneDamage;
extern int xLineCrossed;
extern int xUseSuccess;

enum {
    FIELD_TICS,
    FIELD_RUN,
    FIELD_STRAFE,
    FIELD_TURN,
    FIELD_BUTTONS,
    FIELD_OPTIONS,
    FIELD_CHECKTIC,
    FIELD_PLAYER,
    FIELD_LINE,
    FIELD_RNGINDEX,
    FIELD_USE = 17,
    FIELD_CHECK_BEGIN = 8,
    FIELD_CHECK_END = 15,
    FIELD_SPEED
// TODO: change when more doubles are added
//  FIELD_CHECK_BEGIN_D,
//  FIELD_CHECK_END_D
};

// rather arbitrary limits
enum {
    LEN_CHAR = 256,
    LEN_FINEANGLES = 8192,
    LEN_BUTTONS = 4
};

static const char *savepoint_mode_names[4] = {
    "no save",
    "user",
    "start",
    "auto"
};

char angle_type;
static const double angle_types[4] = {64.0, 90.0, 2048.0, 16384.0};
static const char *angle_names[4] = {
    "byte angles",
    "degrees",
    "fine angles",
    "longtics"
};
static char tic_buf[TIC_LEN];
static char noexit, reinit;
static int ticlist_lines, ticlist_start, ticlist_pos;

static char x_guiSave(void);
static void x_guiLoad(void);
static void x_bruteWindowOpen(void);
const char *keys[MAXCMDS];
static struct {
    const char **s;
    void *func; // fix compiler warnings
    char arglen;
    unsigned char flags;
} const cmds[MAXCMDS] = {
    {&keys[XKEY_FIRE],            x_toggleFire},
    {&keys[XKEY_USE],             x_toggleUse},
    {&keys[XKEY_PAUSE],           x_togglePause},
    {&keys[XKEY_TURN_STEP_LEFT],  x_turnStep,  1, F_ARGLEN_IS_ARG},
    {&keys[XKEY_TURN_STEP_RIGHT], x_turnStep, -1, F_ARGLEN_IS_ARG},
    {&keys[XKEY_PREV_TIC],        x_seekDemo, -1, F_ARGLEN_IS_ARG},
    {&keys[XKEY_NEXT_TIC],        x_seekDemo,  1, F_ARGLEN_IS_ARG},
    {&keys[XKEY_COPY_TIC],        x_copyTic},
    {&keys[XKEY_DELETE_TIC],      x_deleteTic},
    {&keys[XKEY_NEW_TIC],         x_newTic},
    {&keys[XKEY_TOGGLE_BLOOD],    x_toggleBlood},
    {&keys[XKEY_SET_SAVEPOINT],   x_setSavepoint},
    {&keys[XKEY_UNSET_SAVEPOINT], x_unsetSavepoint},
    {&keys[XKEY_LOAD],            x_guiLoad},
    {&keys[XKEY_SAVE],            x_guiSave},
    {&keys[XKEY_EXIT],            x_exitGui},
    {&keys[XKEY_BRUTE],           x_bruteWindowOpen},
    // one digit
    {&keys[XKEY_CHANGE_PLAYER],    x_changePlayerView,  1},
    {&keys[XKEY_TRANSPOSE_LEFT],   x_transposeLeft,     1},
    {&keys[XKEY_TRANSPOSE_RIGHT],  x_transposeRight,    1},
    {&keys[XKEY_CHANGE_WEAPON],    x_changeWeapon,      1},
    {&keys[XKEY_CHANGE_SAVE_GAME], x_changeSaveGame,    1},
    {&keys[XKEY_CHANGE_LOAD_GAME], x_changeLoadGame,    1},
    {&keys[XKEY_SP_STYLE],         x_setSavepointStyle, 1},
    // char
    {&keys[XKEY_RUN_BACKWARD], x_run,    3, F_NEG_ARG},
    {&keys[XKEY_RUN_FORWARD],  x_run,    3},
    {&keys[XKEY_STRAFE_LEFT],  x_strafe, 3, F_NEG_ARG},
    {&keys[XKEY_STRAFE_RIGHT], x_strafe, 3},
    // char and short
    {&keys[XKEY_TURN_LEFT],  x_turn,     5},
    {&keys[XKEY_TURN_RIGHT], x_turn,     5, F_NEG_ARG},
    {&keys[XKEY_TURN_STEP],  x_turnStep, 5},
    // int
    {&keys[XKEY_COPY_TIC_X],    x_copyTicX,   10},
    {&keys[XKEY_DELETE_TIC_X],  x_deleteTicX, 10},
    {&keys[XKEY_BACKWARD_DEMO], x_seekDemo,   10, F_NEG_ARG},
    {&keys[XKEY_FORWARD_DEMO],  x_seekDemo,   10}
};

static struct {
    const char *s;
    int y;
    int x;
    int input_x;
    int input_width;
    char dbl; // whether it can be converted from a double
} const field_vals[BRUTE_FIELDS] = {
    {"Tics",      1, 1, 9, 10},
    {"Run",       2, 1, 9, 10},
    {"Strafe",    3, 1, 9, 10},
    {"Turn",      4, 1, 9, 10},
    {"Buttons",   5, 1, 9, 10},
    {"Options",   6, 1, 9, 10},
    {"Check tic", 1, 20, 30, 10},
    {"Player",    2, 20, 30, 10},
    {"Line",      3, 20, 30, 10},
    {"RNG index", 4, 20, 30, 10},
    {"Damage",    5, 20, 30, 10},
    {"X",         1, 41, 48, 10, true},
    {"Y",         2, 41, 48, 10, true},
    {"Z",         3, 41, 48, 10, true},
    {"X mom.",    4, 41, 48, 10, true},
    {"Y mom.",    5, 41, 48, 10, true},
    {"Speed",     6, 41, 48, 10},
    {"Use",       7, 41, 48, 10}
};

static int brute_fineangles;
static WINDOW *ticlist, *input, *info, *ticinfo, *prompt, *brute, *brutemsg;
static FIELD *input_fields[3], *file_fields[2],
  *brute_fields[2 * BRUTE_FIELDS + 1];
static FIELD **brute_input = &brute_fields[BRUTE_FIELDS];
static FORM *input_form, *file_form, *brute_form;

static void x_formDriver(int c, WINDOW *win, FORM *form);
static char *x_removeSpaces(char *c);
static void x_updateTiclist(void);
static void x_deleteStuff(void);
static void x_parseBrute(void);

// the results are not the best but at least usable
static inline int x_calcLines(int y)
{
    return (float)y / DEFLINES * LINES;
}

static inline int x_calcCols(int x)
{
    return (float)x / DEFCOLS * COLS;
}

void x_initGui(void)
{
    int c, i;
    char space;
    initscr();
    noecho();
    cbreak();

    if (!reinit && (LINES < DEFLINES || COLS < DEFCOLS)) {
        printw("Minimal terminal size for VXDRE is %dx%d. Current size is "
          "%dx%d. Do you want to continue?", DEFCOLS, DEFLINES, COLS, LINES);
        switch (getch()) {
        case KEY_RESIZE:
        case 'y':
            erase();
            break;
        default:
        case 'n':
            endwin();
            return;
        }
    }
    // save/load/exit
    prompt = newwin(0, 0, LINES - 2, 0);
    file_fields[0] = new_field(1, COLS - 19, 0, 19, 0, 0);
    field_opts_off(file_fields[0], O_STATIC);
    set_max_field(file_fields[0], 0);
    file_form = new_form(file_fields);
    set_form_win(file_form, prompt);
    set_form_sub(file_form, derwin(prompt, 0, 0, 0, 0));
    post_form(file_form);

    input = newwin(1, 0, 1, COLS - TICLIST_COLS);
    keypad(input, true);
    input_fields[0] = new_field(1, 6, 0, 0, 0, 0);
    input_fields[1] = new_field(1, 10, 0, 6, 0, 0);
    set_field_opts(input_fields[0], O_VISIBLE | O_PUBLIC | O_AUTOSKIP);
    set_field_buffer(input_fields[0], 0, "Input");
    set_field_back(input_fields[1], A_UNDERLINE);
    input_form = new_form(input_fields);
    set_form_win(input_form, input);
    set_form_sub(input_form, derwin(input, 1, 16, 0, 0));
    post_form(input_form);

    ticlist_lines = LINES - 4;
    ticlist = newwin(LINES - 2, TICLIST_COLS, 0, 0);

    info = newwin(0, 0, 2, 19);
    ticinfo = newwin(0, 19, LINES - 2, 0);

    brute = newwin(x_calcLines(10), 0, x_calcLines(11), TICLIST_COLS);
    brutemsg = derwin(brute, 1, COLS - TICLIST_COLS - 2, CALCYX(8, 1));
    keypad(brute, true);
    for (i = 0; i < BRUTE_FIELDS; i++) {
        brute_input[i] = new_field(1, field_vals[i].input_width,
          CALCYX(field_vals[i].y, field_vals[i].input_x), 0, 0);
        set_field_back(brute_input[i], A_UNDERLINE);
        field_opts_off(brute_input[i], O_STATIC);

        brute_fields[i] = new_field(1, strlen(field_vals[i].s),
          CALCYX(field_vals[i].y, field_vals[i].x), 0, 0);
        set_field_buffer(brute_fields[i], 0, field_vals[i].s);
        set_field_opts(brute_fields[i], O_VISIBLE | O_PUBLIC | O_AUTOSKIP);
    }
    brute_form = new_form(brute_fields);
    set_form_win(brute_form, brute);
    set_form_sub(brute_form, derwin(brute, 0, 0, 0, 0));
    post_form(brute_form);

    refresh();
    wrefresh(input);
    x_updateInfo();

    noexit = true;
    while (noexit) {
        x_formDriver(c = wgetch(input), input, input_form);
        if (c == KEY_RESIZE) {
            reinit = true;
            erase();
            x_deleteStuff();
            x_initGui();
            return;
        }
        space = c == ' ' || c == '\n';
        form_driver(input_form, REQ_VALIDATION);
        if (x_parseCmd(STR(input_fields[1]), space) && noexit) {
            form_driver(input_form, REQ_CLR_FIELD);
            x_updateInfo();
        }
    }
}

// fix the half-arsed ncurses forms
// https://gist.github.com/alan-mushi/c8a6f34d1df18574f643
static void x_formDriver(int c, WINDOW *win, FORM *form)
{
    switch (c) {
    case KEY_DOWN:
        form_driver(form, REQ_NEXT_FIELD);
        form_driver(form, REQ_END_LINE);
        break;
    case KEY_UP:
        form_driver(form, REQ_PREV_FIELD);
        form_driver(form, REQ_END_LINE);
        break;
    case KEY_LEFT:
        form_driver(form, REQ_PREV_CHAR);
        break;
    case KEY_RIGHT:
        form_driver(form, REQ_NEXT_CHAR);
        break;
    case KEY_BACKSPACE:
    case 127:
        form_driver(form, REQ_DEL_PREV);
        break;
    case KEY_DC:
        form_driver(form, REQ_DEL_CHAR);
        break;
    default:
        form_driver(form, c);
        break;
    }
    wrefresh(win);
}

static char *x_removeSpaces(char *c)
{
    char *end;
    while (isspace((unsigned char)*c))
        ++c;
    if (*c == '\0')
        return c;
    end = c + strlen(c) - 1;
    while (end > c && isspace(*end))
        end--;

    *++end = '\0';
    return c;
}

static int x_parseCmdArg(char *c, char space, char arglen)
{
    int i, digits;
    // command keys can contain numbers, that's why I didn't use atoi directly
    for (i = digits = 0; i < strlen(c); i++)
        if (!isdigit((unsigned char)c[i]))
            return -1;
        else
            ++digits;
    return digits == arglen || (space && digits) ? atoi(c) : -1;
}

int x_parseCmd(char *c, char space)
{
    int i, arg;
    void (*f)(int);
    for (i = 0; i < MAXCMDS; i++) {
        if (!strncmp(*cmds[i].s, c, strlen(*cmds[i].s))) {
            f = cmds[i].func;
            if (cmds[i].flags & F_ARGLEN_IS_ARG || !cmds[i].arglen) {
                f(cmds[i].arglen);
                return true;
            }
            if ((arg = x_parseCmdArg(c + strlen(*cmds[i].s), space,
              cmds[i].arglen)) == -1)
                return false;
            arg = cmds[i].flags & F_NEG_ARG ? -arg : arg;
            f(arg);
            return true;
            break;
        }
    }
    return false;
}

const char *x_getTicString(int n)
{
    char *tmp = tic_buf, shift = longtics ? 0 : 8;
    ticcmd_t tic;
    memset(tmp, 0, TIC_LEN);
    if (n >= tic_len[displayplayer])
        return "";
    tic = tics[displayplayer][n];
    if (tic.forwardmove > 0)
        sprintf(tmp, " MF%d", tic.forwardmove);
    else if (tic.forwardmove < 0)
        sprintf(tmp, " MB%d", -tic.forwardmove);
    if (tic.sidemove > 0)
        sprintf(tmp + strlen(tmp), " SR%d", tic.sidemove);
    else if (tic.sidemove < 0)
        sprintf(tmp + strlen(tmp), " SL%d", -tic.sidemove);
    if (tic.angleturn > 0)
        sprintf(tmp + strlen(tmp), " TL%d", tic.angleturn >> shift);
    else if (tic.angleturn < 0)
        sprintf(tmp + strlen(tmp), " TR%d", -tic.angleturn >> shift);
    if (!(tic.buttons & BT_SPECIAL)) {
        if (tic.buttons & BT_ATTACK)
            strcat(tmp + strlen(tmp), " F");
        if (tic.buttons & BT_USE)
            strcat(tmp + strlen(tmp), " U");
        if (tic.buttons & BT_CHANGE)
            sprintf(tmp + strlen(tmp), " G%d",
              ((tic.buttons & BT_WEAPONMASK) >> BT_WEAPONSHIFT) + 1);
    } else {
        switch (tic.buttons & BT_SPECIALMASK) {
        case BTS_PAUSE:
            strcat(tmp + strlen(tmp), " P");
            break;
        case BTS_SAVEGAME:
            sprintf(tmp + strlen(tmp), " SG%d",
              ((tic.buttons & BTS_SAVEMASK) >> BTS_SAVESHIFT) + 1);
            break;
        case BTS_LOADGAME:
            sprintf(tmp + strlen(tmp), " LG%d",
              ((tic.buttons & BTS_SAVEMASK) >> BTS_SAVESHIFT) + 1);
            break;
        }
    }
    return *tmp == '\0' ? "WT" : ++tmp;
}

#define POSINFO(a) (gamestate == GS_LEVEL ? \
  (double)players[displayplayer].mo->a / FRACUNIT : 0)
#define I_PRINTW(y, x, format, ...) mvwprintw(info, CALCYX(y,x), format, \
  ##__VA_ARGS__)
void x_updateInfo(void)
{
    double x, y, z, dx, dy, direction, angle;
    werase(ticinfo);
    werase(ticlist);
    werase(info);

    mvwprintw(ticinfo, 0, 0, "Gametic %d", gametic);
    mvwprintw(ticinfo, 1, 0, "Savepoint %d", x_getSavepointTic(gametic));
    box(ticlist, 0, 0);
    x_updateTiclist();
    x = POSINFO(x);
    y = POSINFO(y);
    z = POSINFO(z);
    dx = x - POSINFO(PrevX);
    dy = y - POSINFO(PrevY);
    if (!dx && !dy) {
        direction = 0;
    } else {
        direction = atan(dx / dy) * 180 / M_PI - 90;
        direction = dy >= 0 ? -direction : -direction + 180;
    }
    angle = angle_types[angle_type];
    I_PRINTW(0, 42, "RNG index %d", rng.rndindex);
    I_PRINTW(2, 1,  "X       %.6f", x);
    I_PRINTW(3, 1,  "X dist. %.6f", dx);
    I_PRINTW(4, 1,  "X mom.  %.6f", POSINFO(momx));
    I_PRINTW(2, 23, "Y       %.6f", y);
    I_PRINTW(3, 23, "Y dist. %.6f", dy);
    I_PRINTW(4, 23, "Y mom.  %.6f", POSINFO(momy));
    I_PRINTW(2, 45, "Z       %.2f", z);
    I_PRINTW(3, 45, "Z dist. %.2f", z - POSINFO(PrevZ));
    I_PRINTW(5, 30, "Distance moved  %.6f", sqrt(dx * dx + dy * dy));
    I_PRINTW(6, 30, "Direction moved %.6f", direction);
    I_PRINTW(7, 30, "Angle           %.0f", gamestate == GS_LEVEL ?
      players[displayplayer].mo->angle * (angle / ANG90) : 0);
    I_PRINTW(8, 30, "(%s)", angle_names[angle_type]);
    I_PRINTW(19, 0, "Used a line: %s", xUseSuccess ? "yes" : "no");
    I_PRINTW(20, 0, "Damage %d", xDoneDamage);
    I_PRINTW(21, 0, "Time %.2f", leveltime / 35.0);
    I_PRINTW(20, 36, "Blood %s", palette_ondamage ? "on" : "off");
    I_PRINTW(21, 36, "Savepoint mode: %s",
      savepoint_mode_names[savepoint_style]);

    wnoutrefresh(ticlist);
    wnoutrefresh(ticinfo);
    wnoutrefresh(info);
    doupdate();
}
#undef I_PRINTW
#undef POSINFO

void x_updateTiclist(void)
{
    int i;
    ticlist_pos = gametic - ticlist_start;
    if (ticlist_pos > ticlist_lines) {
        ticlist_start += ticlist_pos - ticlist_lines;
        ticlist_pos = ticlist_lines;
    } else if (ticlist_pos < 1) {
        ticlist_start += ticlist_pos - 1;
        ticlist_pos = 1;
    }
    for (i = 1; i <= ticlist_lines; i++) {
        if (i == ticlist_pos) {
            wattr_on(ticlist, A_STANDOUT, NULL);
            mvwprintw(ticlist, i, 1, x_getTicString(ticlist_start + i - 1));
            wattr_off(ticlist, A_STANDOUT, NULL);
            continue;
        }
        mvwprintw(ticlist, i, 1, x_getTicString(ticlist_start + i - 1));
    }
}

void x_exitGui(void)
{
    werase(prompt);
    mvwprintw(prompt, 0, 0, "Do you want to save before quit? (y/n/c)");
    switch (wgetch(prompt)) {
    case 'y':
        if (!x_guiSave())
            return;
    case 'n':
        break;
    default:
        return;
    }
    x_deleteStuff();
}

static void x_deleteStuff(void)
{
    int i;
    unpost_form(input_form);
    unpost_form(file_form);
    unpost_form(brute_form);
    free_form(input_form);
    free_form(file_form);
    free_form(brute_form);
    free_field(input_fields[0]);
    free_field(input_fields[1]);
    free_field(file_fields[0]);
    delwin(prompt);
    delwin(ticlist);
    delwin(info);
    delwin(ticinfo);
    delwin(input);
    for (i = 0; i < sizeof(brute_fields) / sizeof(brute_fields[0]); i++)
        free_field(brute_fields[i]);
    noexit = false;
    endwin();
}

static char x_guiSave(void)
{
    int c, e;
    const char *path;
    FILE *fp;
    werase(prompt);
    mvwprintw(prompt, 0, 0, "File to write to:");
    while (true) {
        if ((c = wgetch(prompt)) == '\n')
            break;
        else if (c == 27)
            return false;
        x_formDriver(c, prompt, file_form);
    }
    form_driver(file_form, REQ_VALIDATION);
    path = STR(file_fields[0]);
    if (*path == '\0')
        return false;
    fp = fopen(path, "rb");
    if (fp) {
        mvwprintw(prompt, 1, 0, "Do you want to overwrite file? (y/n)");
        c = wgetch(prompt);
        fclose(fp);
        if (c != 'y') {
            form_driver(file_form, REQ_CLR_FIELD);
            return false;
        }
    }
    fp = fopen(path, "wb");
    e = errno;
    form_driver(file_form, REQ_CLR_FIELD);
    if (!fp) {
        mvwprintw(prompt, 1, 0, "Cannot open file: %s", strerror(e));
        wgetch(prompt);
        return false;
    }
    x_saveDemo(fp);
    return true;
}

static void x_guiLoad(void)
{
    int c, e;
    long size;
    unsigned char *demo_p;
    const char *errstr;
    FILE *fp;
    werase(prompt);
    mvwprintw(prompt, 0, 0, "File to read from:");
    while (true) {
        if ((c = wgetch(prompt)) == '\n')
            break;
        else if (c == 27)
            return;
        x_formDriver(c, prompt, file_form);
    }
    form_driver(file_form, REQ_VALIDATION);
    fp = fopen(STR(file_fields[0]), "rb");
    e = errno;
    form_driver(file_form, REQ_CLR_FIELD);
    if (!fp) {
        mvwprintw(prompt, 1, 0, "Cannot open file: %s.", strerror(e));
        wgetch(prompt);
        return;
    }
    fseek(fp, 0, SEEK_END);
    size = ftell(fp);
    if (size == LONG_MAX || size == -1) { // FIXME?
        errstr = "File size cannot be calculated";
        goto error;
    }
    rewind(fp);
    demo_p = malloc(size);
    if (!demo_p) {
        errstr = "Cannot allocate memory";
        goto error;
    }
    fread(demo_p, 1, size, fp);
    if (!x_openDemo(demo_p, size)) { // frees demo_p
        errstr = "Cannot open demo";
        goto error;
    }
    fclose(fp);
    return;
  error:
    fclose(fp);
    mvwprintw(prompt, 1, 0, "%s.", errstr);
    wgetch(prompt);
}

static void x_bruteWindowOpen(void)
{
    int i, c;
    char tmp[20];
    SDL_Thread *thread;
    box(brute, 0, 0);

    sprintf(tmp, "%d", gametic);
    set_field_buffer(brute_input[FIELD_TICS], 0, tmp);
    for (i = FIELD_RUN; i < BRUTE_FIELDS; i++)
        set_field_buffer(brute_input[i], 0, "0");
    // xdre's order: turn, run, strafe, buttons
    set_field_buffer(brute_input[FIELD_OPTIONS], 0, "{2,0,1,3},0");
    set_field_buffer(brute_input[FIELD_CHECKTIC], 0, tmp);
    sprintf(tmp, "%d", displayplayer);
    set_field_buffer(brute_input[FIELD_PLAYER], 0, tmp);
    werase(brutemsg);
    while (true) {
        x_formDriver(c = wgetch(brute), brute, brute_form);
        switch (c) {
        case 27:
            return;
        case '\n':
            if (!(thread = SDL_CreateThread(x_parseBrute, "Brute", NULL))) {
                mvwprintw(brutemsg, 0, 0, "SDL_CreateThread failed: %s.",
                  SDL_GetError());
                break;
            }
        }
    }
}

static unsigned char x_getCmpCheck(char **s)
{
    unsigned char skip = 1, cmp = CMP_NONE;
    switch (**s) {
    case '=':
        cmp = (*s)[1] == '=' ? ++skip, CMP_EQU : CMP_NONE;
        break;
    case '!':
        cmp = (*s)[1] == '!' ? ++skip, CMP_NEQ : CMP_NONE;
        break;
    case '>':
        cmp = (*s)[1] == '=' ? ++skip, CMP_GEQ : CMP_GTR;
        break;
    case '<':
        cmp = (*s)[1] == '=' ? ++skip, CMP_LEQ : CMP_LSS;
        break;
    }
    if (!cmp)
        return CMP_NONE;
    *s += skip;
    return cmp;
}

// 0 if out of range
static unsigned char x_checkRange(int n, int field)
{
    switch (field) {
    case FIELD_TICS:
    case FIELD_CHECKTIC:
        if (n < 1 || n > tic_len[displayplayer])
            break;
        return true;
    case FIELD_RUN:
        if (n < SCHAR_MIN + 1 || n > SCHAR_MAX)
            break;
        return true;
    case FIELD_STRAFE:
        if (n < SCHAR_MIN || n > SCHAR_MAX)
            break;
        return true;
    case FIELD_TURN:
        if (brute_fineangles) {
            if (n < -FINEANGLES || n > FINEANGLES - 1)
                break;
        } else if (n < SCHAR_MIN || n > SCHAR_MAX) {
            break;
        }
        return true;
    case FIELD_BUTTONS:
        if (n < 0 || n > (BT_ATTACK | BT_USE))
            break;
        return true;
    case FIELD_OPTIONS:
        if (n < 0 || n > MAXTICCMDS - 1)
            break;
        return true;
    case FIELD_PLAYER:
        if (n < 0 || n > MAXPLAYERS - 1 || !playeringame[n])
            break;
        return true;
    case FIELD_LINE:
    case FIELD_RNGINDEX:
        if (n < 0)
            break;
    default:
        return true;
    }
    return false;
}

// 0 = success, 1 = error, 2 = len_err
static unsigned char x_parseBruteCmd(void *mem, char *s, int len, int field,
  char type, int *cmdlen)
{
    char *tmp;
    int val[3], i = 0;
    do {
        if (!(s - 1))
            break;
        if (!x_checkRange(val[0] = atoi(s), field))
            return 1;
        tmp = strchr(s, ':');
        if (tmp && (!strchr(s, ',') || tmp < strchr(s, ','))) {
            if (!x_checkRange(val[1] = atoi(++tmp), field))
                return 1;
            if (val[0] > val[1]) {
                val[2] = val[0];
                val[0] = val[1];
                val[1] = val[2];
            }
            for (; val[0] <= val[1]; val[0]++, i++) {
                if (i >= len)
                    return 2;
                if (!type)
                    ((char *)mem)[i] = val[0];
                else
                    ((short *)mem)[i] = val[0] << (!brute_fineangles * 8);
            }
        } else {
            if (i >= len)
                return 2;
            if (!type)
                ((char *)mem)[i] = val[0];
            else
                ((short *)mem)[i] = val[0] << (!brute_fineangles * 8);
            ++i;
        }
    } while (s = strchr(s, ',') + 1);
    *cmdlen = i;
    return 0;
}

static void x_freeBruteCmds(struct brutecmd *cmds, int len)
{
    int i;
    if (cmds)
        for (i = 0; i < len; i++) {
            free(cmds[i].forwardmove);
            free(cmds[i].sidemove);
            free(cmds[i].angleturn);
            free(cmds[i].buttons);
        }
    free(cmds);
}

static void x_bruteMsg(const char *s)
{
    werase(brutemsg);
    mvwprintw(brutemsg, 0, 0, "%s", s);
    touchwin(brute);
    wrefresh(brutemsg);
}

#define OFFSETS(a) offsetof(struct brutecmd, a), offsetof(ticcmd_t, a), \
  offsetof(struct brutecmd, a##_len)
// doesn't care about invalid characters, yuck
static void x_parseBrute(void)
{
    struct brutecmd *bcmds = NULL;
    struct brutecheck check = {0};
    // i is also used as the number of the invalid field
    int i, j, len = 1, tmp[MAXTICCMDS], cmdlen;
    double d;
    char *buf;
    // uses the order of the fields
    unsigned short cmd_len[MAXTICCMDS] = {LEN_CHAR, LEN_CHAR, 0, LEN_BUTTONS};
    struct {
        unsigned char cmd;
        unsigned char plr;
        unsigned char len;
        unsigned char type;
    } const offsets[MAXTICCMDS] = {
        {OFFSETS(forwardmove)},
        {OFFSETS(sidemove)},
        {OFFSETS(angleturn), true},
        {OFFSETS(buttons)}
    };
    char **tmp_ptr;

    form_driver(brute_form, REQ_VALIDATION);
    for (i = 0; i < BRUTE_FIELDS; i++)
        if (!*STR(brute_input[i]))
            goto error;
    check.tic = atoi(STR(brute_input[FIELD_CHECKTIC]));
    if (!x_checkRange(check.tic, i = FIELD_CHECKTIC))
        goto error;
    check.player = atoi(STR(brute_input[FIELD_PLAYER]));
    if (!x_checkRange(check.player, i = FIELD_PLAYER))
        goto error;
    if (sscanf(STR(brute_input[FIELD_OPTIONS]), "{%d,%d,%d,%d},%d", &tmp[0],
      &tmp[1], &tmp[2], &tmp[3], &brute_fineangles) != 5) {
        i = FIELD_OPTIONS;
        goto error;
    }
    for (i = 0; i < MAXTICCMDS; i++) {
        if (!x_checkRange(tmp[i], FIELD_OPTIONS)) {
            i = FIELD_OPTIONS;
            goto error;
        }
        check.cmd_offset[i] = offsets[i].cmd;
        check.plr_offset[i] = offsets[i].plr;
        check.len_offset[i] = offsets[i].len;
        check.type[i] = offsets[i].type;
    }
    brute_fineangles &= !!longtics; // always use short turns without longtics
    cmd_len[FIELD_TURN - FIELD_RUN] = brute_fineangles ?
      LEN_FINEANGLES : LEN_CHAR;
    check.usecheck = !!atoi(STR(brute_input[FIELD_USE]));
    for (i = FIELD_CHECK_BEGIN; i <= FIELD_CHECK_END; i++) {
        buf = strtok(STR(brute_input[i]), ";");
        for (j = 0; j < MAX_CMP_VALUES; j++) {
            if (!(check.cmp[i - FIELD_CHECK_BEGIN][j] = x_getCmpCheck(&buf)))
                break;
            if (field_vals[i].dbl && (strchr(buf, '.') || strchr(buf, ','))) {
                if ((d = atof(buf)) < -32768.0 || d > 32768.0)
                    goto error;
                check.values[i - FIELD_CHECK_BEGIN][j] = d * FRACUNIT;
            } else if (!x_checkRange(check.values[i - FIELD_CHECK_BEGIN][j] =
              atoi(buf), i)) {
                goto error;
            }
            if (!(buf = strtok(NULL, ";")))
                break;
        }
    }
    buf = strtok(STR(brute_input[FIELD_SPEED]), ";");
    // TODO: change when more doubles are added
    for (i = 0; i < MAX_CMP_VALUES; i++) {
        if (!(check.cmp[SPEED][i] = x_getCmpCheck(&buf)))
            break;
        check.values_d[0][i] = atof(buf);
        if (!(buf = strtok(NULL, ";")))
            break;
    }

    strtok(STR(brute_input[FIELD_TICS]), ";");
    while (strtok(NULL, ";"))
        ++len;
    bcmds = calloc(len, sizeof(*bcmds));
    buf = strtok(STR(brute_input[FIELD_TICS]), ";");
    for (j = 0, i = FIELD_TICS;; j++) {
        if (!x_checkRange(bcmds[j].tic = atoi(buf), FIELD_TICS))
            goto error;
        if (!(buf = strtok(NULL, ";")))
            break;
    }
    for (i = 0; i < MAXTICCMDS; i++) {
        j = 0;
        buf = strtok(STR(brute_input[i + FIELD_RUN]), ";");
        do {
            tmp_ptr = (char **)((char *)&bcmds[j] + offsets[i].cmd);
            if (!offsets[i].type)
                *tmp_ptr = malloc(cmd_len[i]);
            else
                *tmp_ptr = malloc(cmd_len[i] * sizeof(short));
            switch (x_parseBruteCmd(*tmp_ptr, buf, cmd_len[i], i + FIELD_RUN,
              offsets[i].type, &cmdlen)) {
            case 1:
                i += FIELD_RUN;
                goto error;
            case 2:
                goto len_err;
            default:
                break;
            }
            *(short *)((char *)&bcmds[j] + offsets[i].len) = cmdlen;
        } while (++j < len && (buf = strtok(NULL, ";")));
        if (j < len) {
            i += FIELD_RUN;
            goto error;
        }
    }
//  x_bruteMsg("Parsed successfully...");
    if (x_startBrute(bcmds, len, check))
        x_bruteMsg("Found it!");
    else
        x_bruteMsg("No success.");
    x_freeBruteCmds(bcmds, len);
    return;
  error:
    x_freeBruteCmds(bcmds, len);
    werase(brutemsg);
    mvwprintw(brutemsg, 0, 0, "The value of \"%s\" is invalid.",
      field_vals[i].s);
    touchwin(brute);
    wrefresh(brutemsg);
    return;
  len_err: // there is no reason to use duplicate values
    x_freeBruteCmds(bcmds, len);
    werase(brutemsg);
    mvwprintw(brutemsg, 0, 0, "\"%s\" contains more than %d values.",
      field_vals[i + FIELD_RUN].s, cmd_len[i]);
    touchwin(brute);
    wrefresh(brutemsg);
}
#undef OFFSETS

